;;; Code to load/reload x10 configuration data from the database.

(ns x10.config
  [:use [clojure.set :only (difference)]
        [clojure.contrib.sql
	 :only (with-connection with-query-results transaction)]]
  [:require x10.core]
  [:import gnu.io.CommPortIdentifier [com.micheldalal.x10 CM17A]])

;; The *devices* ref holds the devices map for rendering and finding
;; controllers.
(def *devices* (ref {}))

;; The *ports* ref holds the map from port names (aka /dev/*) to open
;; port objects
(def *ports* (ref {}))

;;; Map from controller type names to code.
;; Note that the keys here must match the types in the controllers
;; table in the database. There should be one entry for each unique
;; controller module. It maps to a function to produce a new
;; controller given a port and delay.
(def +controller-type-map+
  {"CM17A" (fn [port delay] (x10.core.CM17A-controller. (new CM17A) port delay))})

;; Close any open ports as we shut down, or before reloading.
(defn close-ports [] (map #(.close %) (vals @*ports*)))

(defn make-ports-map [new old]
  [(apply hash-map
	  (mapcat #(if-let [port (old %)] [% port]
			   [% (.open (CommPortIdentifier/getPortIdentifier %)
				     "Mike's X10 Controller" 2000)])
		  new))
   (map old (difference (set (keys old)) (set new)))])

;; Load the device database into a set of lists.
;; Since the results are lazy, we have to force them with doall before
;; getting the next set of results.
(defn load-database [db-file]
  (with-connection  {:classname "org.sqlite.JDBC" :subprotocol "sqlite"
		     :subname db-file}
    (transaction
     [(with-query-results controllers ["select * from controllers"]
	(doall controllers))
      (with-query-results names ["select * from names order by name"]
	(doall names))
      (with-query-results groups ["select * from groups order by name"]
	(doall groups))
      (with-query-results ports ["select distinct port from controllers"]
	(doall ports))
      (with-query-results codes ["select * from code"] (doall codes))])))

;; Load the device database in db-file into the *devices* map.
(defn load-devices [db-file]
  (let [[controllers names groups ports codes] (load-database db-file)
	make-map (fn [f list] (apply sorted-map (mapcat f list)))
	controller-map (make-map (fn [{:keys [name module port delay]}]
				   [name (agent ((+controller-type-map+ module)
						 port delay)
						:error-mode :continue)])
				 controllers)
	name-map (make-map (fn [{:keys [name controller code unit]}]
			     [name (x10.core.Module. (controller-map controller)
						     name code unit (atom nil))])
			   names)
	group-map (make-map (fn [[name group]]
			      [name (x10.core.Group. name (map name-map group))])
			    (map (fn [[key vals]] [key (map :device vals)])
				 (group-by :name groups)))
	code-map (conj (make-map (fn [{:keys [name command]}]
				   [name (x10.core.Command. name command)])
				 codes)
		       {"Reload" (x10.core.Command.
				  "Reload" (str "(x10.config/load-devices \""
						db-file "\")"))})]
    (doall
     (map #(.close %) 
	  (dosync
	   (let [[ports-map closing] (make-ports-map (map :port ports)
						     (ensure *ports*))]
	     (doall (map #(x10.core/set-port @% ports-map) (vals controller-map)))
	     (ref-set *ports* ports-map)
	     (ref-set *devices* {"Devices" name-map "Groups" group-map
				 "Code" code-map})
	     closing))))))
